Pro ASP.NET Core MVC2(第7版)翻译

第24章:使用表单标签助手

作者:Adam Freeman
翻译:陈广
日期:2018-10-13


MVC 提供了一组内置标签助手,用于对 HTML 元素执行通常需要的转换。本章我将描述在 HTML 表单上操作的标签助手,包括forminputlabelselectoptiontextarea元素。在第25章中,我描述了其他内置标签助手,它们提供了与表单无关的特性。表24-1为表单标签助手简历。

表 24-1:表单标签助手简历

问题 回答
它们是什么? 表单标签助手用于转换 HTML 表单元素,这样您就不必编写自定义标签助手来解决最常见的问题。
它们有何用途? 表单标签助手确保 HTML 表单元素(包括表单中的元素,如labelinput)是一致生成的。在大多数情况下,标签助手确保直接使用视图模型类设置idnamefor等重要属性,但一些标签助手也可以生成内容,例如使用option元素填充select元素。
如何使用它们? 内置标签助手查找以asp-(如asp-for)为前缀的属性。
是否有任何缺陷或限制? 唯一的限制是必须向标签助手提供模型数据以在select元素中生成option元素。在《使用 Select 和 Option 元素》一节中,我描述了这个问题,并提供了一个自定义标签助手来解决这个问题。
有没有其他选择? 您可以在视图中编写 HTML 表单,完全不需要使用标签助手属性。您也可以使用我在第23章中描述的技术编写自己的标签助手。

表24-2为本章摘要。

表 24-2:本章摘要

问题 解决方案 清单
在表单元素上设置action属性 使用表单元素标签助手 5
防止跨站请求伪造 给 action 方法应用ValidateAntiForgeryToken特性,并在表单元素上可选地将asp-antiforgery属性设置为true 6、7
input元素上设置idnamevalue属性 应用asp-for属性 8
格式化input元素显示的值 asp-format属性应用于input元素或在模型类中应用DisplayFormat属性。 9-12
设置label元素的for属性和内容 应用asp-for属性 13
更改已应用asp-for属性的label元素的内容 Display属性应用于模型类属性,并使用Name属性指定内容 14
select元素上设置idname属性 应用asp-for属性 15
生成option元素 应用asp-items属性 16-21
textarea元素上设置idname属性 应用asp-for属性 22、23

准备示例项目

本章我继续使用我在第23章中创建的Cities项目。对于本章,我希望启用 MVC 附带的内置标签助手,并禁用我在第23章中创建的自定义助手。清单24-1显示了我对视图导入文件所做的更改,在该文件中,我将 Cities 程序集中的助手类的@addTagHelper表达式替换为一个设置 MVC 标签助手的表达式,该表达式是在一个名为 Microsoft.AspNetCore.Mvc.TagHelpers 的程序集中定义的。

清单 24-1:Views 文件夹下的 _ViewImports.cshtml 文件,更改标签助手

@using Cities.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

重置视图和布局

清单24-2显示了 Index.cshtml 视图的内容,在该视图中,我删除了自定义标签助手类使用的属性。

清单 24-2:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model IEnumerable<City>

@{ Layout = "_Layout"; }

<table class="table table-sm table-bordered">
    <thead class="bg-primary text-white">
        <tr>
            <th>Name</th>
            <th>Country</th>
            <th class="text-right">Population</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var city in Model)
        {
            <tr>
                <td>@city.Name</td>
                <td>@city.Country</td>
                <td class="text-right">@city.Population?.ToString("#,###")</td>
            </tr>
        }
    </tbody>
</table>
<a href="/Home/Create" class="btn btn-primary">Create</a>

清单24-3显示了对 Create.cshtml 文件的相应更改,我已经返回到使用标准 HTML 元素,而不使用第23章中使用的属性。

清单 24-3:Views/Home 文件夹下的 Create.cshtml 文件的内容

@model City

@{ Layout = "_Layout"; }

<form method="post" action="/Home/Create">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" name="Name" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" name="Country" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" name="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

最后是对共享布局的更改,如清单24-4所示。

清单 24-4:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Cities</title>
    <link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    <div>@RenderBody()</div>
</body>
</html>

如果运行应用程序,您将看到城市列表,您可以单击【Create】按钮并填写表单向服务器提交新数据,如图24-1所示。

图24-1 运行示例应用程序

使用表单元素

FormTagHelper类是form元素的内置标记助手,用于管理 HTML 表单的配置,以便它们根据应用程序的路由配置确定正确的 action 方法。此签记助手支持表24-3中描述的属性。

表 24-3:表单元素的内置标签助手属性

名称 描述
asp-controller 此属性用于为action属性 URL 的路由系统指定controller值。如果省略,则将使用渲染视图的控制器。
asp-action 此属性用于为action属性 URL 的路由系统指定action值的 action 方法。如果省略,则将使用渲染视图的 action。
asp-route-* 名称以asp-route-开头的属性用于为action属性 URL 指定附加值,以便asp-route-id属性用于向路由系统提供id段的值。
asp-route 此属性用于指定将用于为action属性生成 URL 的路由的名称。
asp-area 此属性用于指定将用于为action属性生成 URL 的 area 的名称。
asp-antiforgery 此属性控制是否将防伪信息添加到视图中,如《使用防伪功能》一节所述。

设置表单目标

FormTagHelper类的主要目的是使用应用程序的路由配置设置form元素的action属性,确保表单数据总是发送至正确的 URL,甚至当路由架构更改时。在清单24-5中,我使用了asp-actionasp-controller属性来针对 Home 控制器上的Create action 方法。

注意:标签助手不设置method属性,如果从form元素中省略它,浏览器将使用 GET 请求将表单数据发送到客户端。正如我在第17章中解释的那样,如果使用表单数据来修改应用程序中的数据,这可能会导致问题。设置method属性是很好的做法,即使您只是想要 GET 请求,这表示了您还没有忘记选择一个方法类型。

清单 24-5:Views/Home 文件夹下的 Create.cshtml 文件,设置表单目标

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" name="Name" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" name="Country" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" name="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

如果运行应用程序,请求 /Home/Create URL,并检查发送给客户端的 HTML,您将看到标签助手向form元素添加了一个 action 属性,并使用路由系统设置其值,如下所示:

<form method="post" action="/Home/Create">

使用防伪特性

跨站请求伪造(CSRF)是一种利用 Web 应用程序对用户请求进行身份验证的方法。大多数Web应用程序(包括使用 ASP.NET Core 创建的应用程序)都使用 cookies 来识别哪些请求与特定会话相关,而用户身份通常与特定会话相关联。

CSRF(也称session riding)在http://en.wikipedia.org/wiki/Cross-site_request_forgery中有详细描述,它依赖于用户在使用 Web 应用程序之后访问恶意网站,并且不通过单击注销按钮显式结束他们的会话。应用程序仍然认为用户的会话处于活动状态,浏览器存储的 cookie 还没有过期。恶意站点包含一些 JavaScript 代码,该代码在未经用户同意的情况下向应用程序发送表单请求以执行操作,其中操作的类型将取决于受到攻击的应用程序。由于 JavaScript 代码是由用户的浏览器执行的,所以对应用程序的请求包括会话 cookie,并且应用程序在用户不知情或同意的情况下执行操作。

如果form元素不包含action属性 —— 因为它是从具有asp-controllerasp-acton属性的路由系统生成的 —— 那么FormTagHelper类将自动启用反 CSRF 特性,通过将安全令牌添加到表单中隐藏的input元素,并与 cookie 一起添加到发送到客户端的 HTML 中。只有当请求同时包含 cookie 和表单中的隐藏值(恶意站点无法访问)时,应用程序才会处理该请求。表单的每个请求都会生成一组新的唯一的安全令牌。

如果运行应用程序,请求 /Home/Create URL,并查看发送到浏览器的 HTML,您将看到一个隐藏的input元素,如下所示:

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8KuVkH8hFlRApe
    FBxTrhCFTKZe0B9BKwnWDJqLRUDk__PrEwaeCJmiBbGkwW1ZI816c_TrM5XQkJBeqNI5IL8FhuO
    RvjZuYIL-GZvnWZ62OThsZYT02HNX_Lu5LWDNWDdVoS5O5hZtzaoHLeY5lNto" />

如果您使用浏览器的 F12 工具,还可以看到相应的 cookie 添加到响应中。将安全令牌添加到 HTML 响应只是进程的一部分;它们还必须由控制器验证,如清单24-6所示。

清单 24-6:Controllers 文件夹下的 HomeController.cs 文件,验证防伪令牌

using Microsoft.AspNetCore.Mvc;
using Cities.Models;

namespace Cities.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public ViewResult Index() => View(repository.Cities);

        public ViewResult Create() => View();

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Create(City city)
        {
            repository.AddCity(city);
            return RedirectToAction("Index");
        }
    }
}

ValidateAntoForgeryToken属性确保请求包含有效的反 CSRF 令牌,如果它们不存在或不包含预期值,则会抛出异常。

FormTagHelper类提供asp-antiforgery属性来覆盖默认的反 CSRF 行为。如果它的属性值设为true,则即使form元素具有action属性,安全令牌也将包含在响应中。如果属性值为false,那么安全令牌将被禁用。在清单24-7中,我已经显式地启用了该特性,尽管无论如何都会添加安全令牌,因为form元素上没有定义action属性。

清单 24-7:Views/Home 文件夹下的 Create.cshtml 文件,启用反 CSRF 特性

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" name="Name" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" name="Country" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" name="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

提示:测试反 CSRF 功能需要一些诡计。我通过请求包含表单的 URL(如 /Home/Create),然后使用浏览器的F12开发工具定位并从表单中删除隐藏的input元素(或者更改元素的值)。当我填充表单并将其发送到应用程序时,浏览器没有部分所需数据的,请求将失败并显示一个错误页面。

使用 Input 元素

input元素是 HTML 表单的支柱,它提供了用户可以为应用程序提供非结构化数据的主要手段。InputTagHelper类用于转换input元素,以便反映它们收集的视图模型属性的数据类型和格式。使用到的属性在表24-4中描述。

表 24-4:Input 元素的内置标签助手

名称 描述
asp-for 此属性用于指定input元素表示的视图模型属性
asp-format 此属性用于为input元素表示的视图模型属性的值指定格式

配置 Input 元素

asp-for属性用于设置图模型属性的名称,然后会设置input元素的nameidtypevalue属性。在清单24-8中,我将asp-for属性应用于 Create.cshtml 视图中的input元素。

清单 24-8:Views/Home 文件夹下的 Create.cshtml 文件,配置 Input 元素

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" asp-for="Country" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

如果运行应用程序并请求 /Home/Create URL,您将看到标签助手使用asp-for属性指定的属性来裁剪每个input元素,如以下片段(其中省略了反 CSRF 安全令牌):

<form method="post" action="/Home/Create">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" type="text" id="Name" name="Name" value="" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" type="text" id="Country"
            name="Country" value="" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" type="number" id="Population"
            name="Population" value="" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

input元素的type属性告诉浏览器如何在表单中显示元素。您可以在Population属性的input元素中看到此过程的简单结果,此类型属性已被设置为number。这是因为Population属性的类型是int?,标签助手使用type属性指示浏览器仅接收数字值。

注意type属性的解释方式留给浏览器。并非所有浏览器都响应 HTML5 规范中定义的所有类型值,即使这样做,它们的实现方式也有差异。type属性可以为您在表单中期望的数据类型提供有用的提示,但您应该使用模型验证功能来确保用户提供可用数据,如第27章所述。

表24-5描述了用于设置input元素类型属性的不同 C# 属性类型。

表 24-5:C# 属性类型和它们生成的 Input 类型元素

C# 类型 Input 元素类型属性
byte, sbyte, int, uint,short, ushort, long,ulong number
float, double, decimal text,以及用于模型验证的附加属性,如下文所述
bool checkbox
string text
DateTime datetime

floatdoubledecimal类型产生input元素的类型为text,因为并非所有浏览器都允许可用于表示该类型的合法值的所有字符。为了向用户提供帮助,标签助手将属性添加到与模型验证功能一起使用的input元素,我在第27章中对此进行了描述。

您可以通过在input元素上定义type属性来覆盖表24-5中显示的默认映射。标签助手不会覆盖您定义的值,它允许您利用可用的不同input元素类型(如passwordhidden)或 HTML5 中添加的新类型(如number)。

这种方法的一个缺点是,您必须记住在为给定模型属性生成input元素的所有视图中设置type属性。如果需要覆盖多个视图中的默认映射,可以对 C# 模型类中的属性应用UIHint特性,指定表24-6中的一个值作为特性参数。

提示:如果模型属性不是表24-5中的类型之一,并且没有使用UIHint特性修饰,则标签助手将input元素的type属性设置为text

表 24-6:UIHint 参数及它们所生成的 Input 类型元素

输入元素类型属性
HiddenInput hidden
Password password
Text text
PhoneNumber tel
Url url
EmailAddress email
Time time(此值用于显示DateTime对象的时间组件)
Date date(此值用于显示DateTime对象的时间组件)
DateTime-local datetime-local(此值用于在不提供时区信息的情况下显示DateTime对象。)

格式化数据值

当 action 方法为视图提供一个视图模型对象时,标签助手使用给定给asp-for属性的属性值来设置input元素的值属性。asp-format属性用于指定如何格式化该数据值。

为了演示,我向 Home 控制器添加了一个新的 action 方法,如清单24-9所示。action 方法从存储库中选择第一个City对象,并使用它作为创建视图的视图模型。

清单 24-9:Controllers 文件夹下的 HomeController.cs 文件,添加 Action 方法

using Microsoft.AspNetCore.Mvc;
using Cities.Models;
using System.Linq;

namespace Cities.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public ViewResult Index() => View(repository.Cities);

        public ViewResult Edit() => View("Create", repository.Cities.First());

        public ViewResult Create() => View();

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Create(City city)
        {
            repository.AddCity(city);
            return RedirectToAction("Index");
        }
    }
}

如果运行应用程序,请求 /Home/Edit URL,并检查已发送到浏览器的 HTML,您将看到使用视图模型对象填充了值属性,如下所示:

<input class="form-control" type="number" id="Population"
    name="Population" value="8539000" />

asp-format属性接受将传递给标准 C# 字符串格式系统的值,如清单24-10所示。

清单 24-10:Views/Home 文件夹下的 Create.cshtml 文件,格式化数据值

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" asp-for="Country" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" asp-for="Population" asp-format="{0:#,###}" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

属性值是逐字使用的,这意味着必须包含大括号字符和0:引用以及所需的格式。如果您运行应用程序并请求 /Home/Edit URL,将看到填充值已被格式化如下:

<input class="form-control" type="number" id="Population"
    name="Population" value="8,539,000" />

使用此功能时应谨慎,因为您必须确保应用程序的其余部分配置为支持您使用的格式。在本例中,我通过格式化Population值创建了一个问题。标签助手已经将input元素的type属性设置为number,使用表24-5中为Population属性描述的默认映射,但是我指定的格式字符串生成了一个包含非数字字符的value属性。结果是尊循number元素类型的浏览器(记住,并非所有浏览器)可能不会在元素中显示任何值。

您还必须确保应用程序能够以您使用的格式解析值。示例应用程序希望接收一个可以解析为intPopulation值,而包含非数字字符的值将导致验证错误,如第27章所述。

通过模型类应用格式

如果您总是想要对一个模型属性使用相同的格式,那么可以使用DisplayFormat特性装饰 C# 类,此特性定义于System.ComponentModel.DataAnnotations命名空间。DisplayFormat特性需要两个参数来设置数据值的格式:DataFormatString参数指定格式化字符串,ApplyFormatInEditMode参数指定在编辑值时使用格式化。在清单24-11中,我使用DisplayFormat特性修饰了Population属性,使用了应用程序和浏览器都可以作为数字处理的格式。

清单 24-11:Models 文件夹下的 City.cs 文件,将格式化特性应用于模型类

using System.ComponentModel.DataAnnotations;

namespace Cities.Models
{
    public class City
    {
        public string Name { get; set; }
        public string Country { get; set; }

        [DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
        public int? Population { get; set; }
    }
}

asp-format属性优先于DisplayFormat特性,因此我从视图中删除了该属性,如清单24-12所示。

清单 24-12:Views/Home 文件夹下的 Create.cshtml 文件,移除格式化属性

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label for="Name">Name:</label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label for="Country">Country:</label>
        <input class="form-control" asp-for="Country" />
    </div>
    <div class="form-group">
        <label for="Population">Population:</label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

如果运行应用程序并请求 /Home/Edit URL,您将看到Population值已被格式化为两个部分,如下所示:

<input class="form-control" type="number" id="Population"
    name="Population" value="8539000.00" />

使用 Label 元素

label元素由LabelTagHelper类进行转换,该类使用视图模型类来确保标签没的无错误和一致性。它只有一个受支持的属性,如表24-7所述。

表 24-7:Label 元素的内置标签助手

名称 描述
asp-for 此属性用于指定label元素表示的视图模型属性

标签助手将使用视图模型属性的名称来设置for属性的值和label元素的内容。在清单24-13中,我已经将asp-for属性应用于表单中的label元素,这些元素将由标签助手转换。

清单 24-13:Views/Home 文件夹下的 Create.cshtml 文件,应用 Label 标签助手

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label asp-for="Country"></label>
        <input class="form-control" asp-for="Country" />
    </div>
    <div class="form-group">
        <label asp-for="Population"></label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

由于label元素是空的,标签助手将使用模型属性名称作为元素的内容,并设置for属性,该属性告诉浏览器每个label与哪个input元素相关联。如果运行该示例,请求 /Home/Create 或 /Home/Edit URL,并检查发送到浏览器的 HTML,您将看到以下输出元素:

<form method="post" action="/Home/Create">
    <div class="form-group">
        <label for="Name">Name</label>
        <input class="form-control" type="text" id="Name"
            name="Name" value="London" />
    </div>
    <div class="form-group">
        <label for="Country">Country</label>
        <input class="form-control" type="text" id="Country"
            name="Country" value="UK" />
    </div>
    <div class="form-group">
        <label for="Population">Population</label>
        <input class="form-control" type="number" id="Population"
            name="Population" value="8539000.00" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

通过将Display属性应用到模型类属性,可以覆盖用作label元素内容的值,如清单24-14所示。

清单 24-14:Models 文件夹下的 City.cs 文件,更改模型属性描述

using System.ComponentModel.DataAnnotations;

namespace Cities.Models
{
    public class City
    {
        [Display(Name = "City")]
        public string Name { get; set; }

        public string Country { get; set; }

        [DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
        public int? Population { get; set; }
    }
}

Name参数指定要使用的值,而不是属性名称。如果您运行该示例,请求 /Home/Create URL,并检查发送到浏览器的 HTML,将看到label元素的内容发生了更改,如下所示:

<div class="form-group">
    <label for="Name">City</label>
    <input class="form-control" type="text" id="Name" name="Name" value="London" />
</div>

注意,for属性的值没有更改,因此浏览器知道label元素与特定的input元素相关联,该input元素不受Display属性的影响。

提示:您可以通过自己定义label元素来阻止标签助手设置标签元素的内容。如果您希望label元素包含的不仅仅是属性的名称,这是非常有用的,这就是内置标签助手能够提供的全部内容。

使用 Select 和 Option 元素

selectoption元素用于为用户提供一组固定的选项,而不是使用input元素可以打开的数据条目。input负责转换select元素,并支持表24-8中描述的属性。

表 24-8:select 元素的内置标签助手属性

名称 描述
asp-for 此属性用于指定select元素表示的视图模型属性
asp-items 此属性用于指定select元素中包含的选项元素值的源。

asp-for属性设置了forid属性的值,以反映它接收的模型属性。在清单24-15中,我已经将用于Country属性的input元素替换为定义了asp-for属性的select元素。

清单 24-15:Views/Home 文件夹下的 Create.cshtml 文件,使用 select 元素

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label asp-for="Country"></label>
        <select class="form-control" asp-for="Country">
            <option disabled selected value="">Select a Country</option>
            <option>UK</option>
            <option>USA</option>
            <option>France</option>
            <option>China</option>
        </select>
    </div>
    <div class="form-group">
        <label asp-for="Population"></label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

我已经用option元素手动填充了select元素,这些option元素为用户提供了一系列国家供用户选择。如果您运行应用程序并请求 /Home/Create URL,将看到发送到浏览器的 HTML 包含以下select元素:

<select class="form-control" id="Country" name="Country">
    <option disabled selected value="">Select a Country</option>
    <option>UK</option>
    <option>USA</option>
    <option>France</option>
    <option>China</option>
</select>

如果您请求 /Home/Edit URL 并检查发送到浏览器的 HTML,您将看到视图模型对象的Country属性的值已用于更改所选的option元素,如下所示:

<select class="form-control" id="Country" name="Country">
    <option disabled selected value="">Select a Country</option>
    <option selected="selected">UK</option>
    <option>USA</option>
    <option>France</option>
    <option>China</option>
</select>

选择option元素的任务由OptionTagHelper类执行,该类通过TagHelperContext.Items集合接收来自SelectTagHelper的指令。正如我在第23章中解释的那样,这个集合由需要协同工作的标签助手使用,当我创建一个自定义标签助手时,我利用SelectTagHelper在下一节中添加到Items集合中的数据来解决内置的一个限制。

使用数据源填充 select 元素

显式定义select元素的option元素是一种有用的方法,在那些始终具有相同的可能值的情况下使用。但如需要提供从数据模型中获取的选项,或在多个视图中需要相同的选项集,且不希望手动维护重复内容的选项的情况下,这种方法就没有用了。

从枚举生成选项元素

如果您有一组固定的选项要呈现给用户,并且不想在整个应用程序的视图中重复这些选项,那么可以使用枚举。我在 Models 文件夹中添加了一个名为 CountryNames.cs 的类文件,并使用它来定义清单24-16所示的枚举。

清单 24-16:Models 文件夹下的 CountryNames.cs 文件的内容

namespace Cities.Models
{
    public enum CountryNames
    {
        UK,
        USA,
        France,
        China
    }
}

您不能在asp-items属性中直接使用枚举,因为标签助手希望使用一系列SelectListItem对象。然而,有一个方便的助手方法可以执行所需的转换,如清单24-17所示。

清单 24-17:Views/Home 文件夹下的 Create.cshtml 文件,使用枚举

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label asp-for="Country"></label>
        <select class="form-control" asp-for="Country"
                asp-items="@new SelectList(Enum.GetNames(typeof(CountryNames)))">
            <option disabled selected value="">Select a Country</option>
        </select>
    </div>
    <div class="form-group">
        <label asp-for="Population"></label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

当使用枚举时,生成option元素的最佳方法是为asp-items属性提供一个由枚举值名称填充的SelectList对象。在幕后,SelectTagHelper类从IEnumerable<SelectListItem>生成option元素,SelectList类实现了这个接口。

如果运行应用程序并请求 /Home/Create 或 /Home/Edit URL,您将看到发送到浏览器的 HTML 包含一组与枚举中的值相对应的option元素,如下所示:

<select class="form-control" id="Country" name="Country">
    <option disabled selected value="">Select a Country</option>
    <option>UK</option>
    <option>USA</option>
    <option>France</option>
    <option>China</option>
</select>

请注意,标签助手只保留了占位符option元素。您显式定义的任何option元素都保持不变,这意味着您不必将占位符与数据值混合。

从模型中生成选项元素

如果需要生成option元素以反映模型中的数据,那么最简单的方法是通过 view bag 提供生成元素所需的数据,如清单24-18所示。

清单 24-18:Controllers 文件夹下的 HomeController.cs 文件,使用 View Bag 提供数据

using Microsoft.AspNetCore.Mvc;
using Cities.Models;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Cities.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public ViewResult Index() => View(repository.Cities);
        public ViewResult Edit()
        {
            ViewBag.Countries = new SelectList(repository.Cities
                .Select(c => c.Country).Distinct());
            return View("Create", repository.Cities.First());
        }

        public ViewResult Create()
        {
            ViewBag.Countries = new SelectList(repository.Cities
                .Select(c => c.Country).Distinct());
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Create(City city)
        {
            repository.AddCity(city);
            return RedirectToAction("Index");
        }
    }
}

EditCreate操作方法将ViewBag.Countries属性设置为一个SelectList对象,该对象使用存储库中的City.Country属性的唯一值填充。在清单24-19中,我使用asp-items属性告诉标签助手从该 view bag 性中获取option元素的数据。

清单 24-19:Views/Home 文件夹下的 Create.cshtml 文件,在 option 元素内使用 View Bag

@model City

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label asp-for="Country"></label>
        <select class="form-control" asp-for="Country" asp-items="ViewBag.Countries">
            <option disabled selected value="">Select a Country</option>
        </select>
    </div>
    <div class="form-group">
        <label asp-for="Population"></label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

如果运行应用程序并请求 /Home/Create 或 /Home/Edit URL,您将看到创建了如下option元素:

<select class="form-control" id="Country" name="Country">
    <option disabled selected value="">Select a Country</option>
    <option selected>UK</option>
    <option>USA</option>
    <option>France</option>
</select>

使用自定义标签助手从模型中生成 Option 元素

通过 view bag 传递option元素所需的数据的问题是,必须记得在渲染使用标签助手的视图的每个 action 方法中生成数据。这会导致代码重复,您可以在清单24-18中了解到这一点,并使正确测试和维护控制器变得更加困难。

一个更好的方法是创建一个自定义标签助手来补充内置的SelectTagHelper类,我将一个名为 SelectOptionTagHelper.cs 的类文件添加到 Infrastructure/TagHelper 文件夹中,并定义了清单24-20中所示的类。

清单 24-20:Infrastructure/TagHelper 文件夹下的 SelectOptionTagHelper.cs 文件

using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Cities.Models;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Cities.Infrastructure.TagHelpers
{
    [HtmlTargetElement("select", Attributes = "model-for")]
    public class SelectOptionTagHelper : TagHelper
    {
        private IRepository repository;

        public SelectOptionTagHelper(IRepository repo)
        {
            repository = repo;
        }

        public ModelExpression ModelFor { get; set; }
        public override async Task ProcessAsync(TagHelperContext context,
        TagHelperOutput output)
        {
            output.Content.AppendHtml(
                (await output.GetChildContentAsync(false)).GetContent());
            string selected = ModelFor.Model as string;
            PropertyInfo property = typeof(City)
                .GetTypeInfo().GetDeclaredProperty(ModelFor.Name);
            foreach (string country in repository.Cities
                .Select(c => property.GetValue(c)).Distinct())
            {
                if (selected != null && selected.Equals(country,
                StringComparison.OrdinalIgnoreCase))
                {
                    output.Content
                        .AppendHtml($"<option selected>{country}</option>");
                }
                else
                {
                    output.Content.AppendHtml($"<option>{country}</option>");
                }
            }
            output.Attributes.SetAttribute("Name", ModelFor.Name);
            output.Attributes.SetAttribute("Id", ModelFor.Name);
        }
    }
}

此标签助手使用model-for属性对select元素进行操作,并使用依赖注入接收存储库对象,该对象可以独立于渲染视图的控制器访问模型数据。此标签助手定义了异步ProcessAsync方法,因为它简化了获取和保存select元素的任何现有内容的过程,该过程是通过GetChildContentAsync方法完成的。

SelectTagHelper指示option元素的名称,这些option元素应该使用自己的类型作为键,通过Items集合中的条目进行选择。标签助手获取所选项的列表,并将其与 LINQ 查询的结果结合使用,为存储库中的每个唯一值生成option元素。在清单24-21中,我更新了select元素,以便将asp-items属性替换为model-for属性,并添加了一个@addTagHelper表达式,该表达式仅为该视图启用自定义标签助手。

清单 24-21:Views/Home 文件夹下的 Create.cshtml 文件,启用自定义标签助手

@model City
@addTagHelper Cities.Infrastructure.TagHelpers.SelectOptionTagHelper, Cities

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label asp-for="Country"></label>
        <select class="form-control" model-for="Country">
            <option disabled selected value="">Select a Country</option>
        </select>
    </div>
    <div class="form-group">
        <label asp-for="Population"></label>
        <input class="form-control" asp-for="Population" />
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

新的标签助手生成相同的输出,但不需要内置助手所需的 view bag 数据。我喜欢这种方法,因为它使 action 方法集中于它们的特定任务,并保持应用程序的总体形状。

使用 Text Areas

textarea元素用于从用户那里获取大量文本,通常用于非结构化数据,如注释或言论。TextAreaTagHelper负责转换textarea元素,并支持表24-9中描述的单个属性。

表 24-9:TextArea 元素的内置标签助手属性

名称 描述
asp-for 此属性用于指定表现textarea元素的视图模型属性

TextAreaTagHelper相对简单,为asp-for属性提供的值用于在textarea元素上设置idname属性。为了演示这个标签助手,我向City模型类添加了一个新属性,如清单24-22所示。

清单 24-22:Models 文件夹下的 City.cs 文件,添加属性

using System.ComponentModel.DataAnnotations;

namespace Cities.Models
{
    public class City
    {
        [Display(Name = "City")]
        public string Name { get; set; }

        public string Country { get; set; }

        [DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
        public int? Population { get; set; }

        public string Notes { get; set; }
    }
}

在清单24-23中,我使用asp-for属性将元素与City类的Notes属性关联起来,在Create.cshtml视图中添加了textarea元素。

清单 24-23:Views/Home 文件夹下的 Create.cshtml 文件,添加 Text Area

@model City
@addTagHelper Cities.Infrastructure.TagHelpers.SelectOptionTagHelper, Cities

@{ Layout = "_Layout"; }

<form method="post" asp-controller="Home" asp-action="Create"
      asp-antiforgery="true">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input class="form-control" asp-for="Name" />
    </div>
    <div class="form-group">
        <label asp-for="Country"></label>
        <select class="form-control" model-for="Country">
            <option disabled selected value="">Select a Country</option>
        </select>
    </div>
    <div class="form-group">
        <label asp-for="Population"></label>
        <input class="form-control" asp-for="Population" />
    </div>
    <div class="form-group">
        <label asp-for="Notes"></label>
        <textarea class="form-control" asp-for="Notes"></textarea>
    </div>
    <button type="submit" class="btn btn-primary">Add</button>
    <a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>

如果运行应用程序并请求 /Home/Create 或 /Home/Create URL,您将看到发送到浏览器的 HTML 包含如下所示的textarea元素:

<div class="form-group">
    <label for="Notes">Notes</label>
    <textarea id="Notes" name="Notes"></textarea>
</div>

TextAreaTagHelper相对简单,但它提供了与我在本章中描述的其他form元素标签助手的一致性。

理解验证表单标签助手

还有两个与 HTML 表单相关的标签助手,我在表24-10中描述了它们,但我会在第27章中更详细地描述他们。当用户提供的数据不符合应用程序的期望时,这些助手用于向用户提供反馈。

表 24-10:验证标签助手类

名称 描述
ValidationMessage 此标签助手用于提供有关单个表单元素的验证反馈。
ValidationSummary 此标签助手用于提供有关表单中所有元素的验证反馈。

总结

本章我描述了用于转换 HTML 表单元素的内置标签助手。这些标签助手确保表单是直接从模型类生成的,这减少了出错的可能性,并为编写 Razor 视图提供了一致的方法。在下一章中,我将描述对一系列 HTML 元素进行操作的其余内置标签助手。

;

© 2018 - IOT小分队文章发布系统 v0.3